
在現代監控和可觀測性領域,Grafana 已成為不可或缺的視覺化平台。隨著組織規模的擴大和監控需求的增加,有效管理 Grafana 的資料來源變得越來越具有挑戰性。本文將開始深入探討如何利用 Terraform 實現基礎設施即程式碼(IaC)的方法,設計一個強大的頂層結構來管理 Grafana 資料來源。
在深入技術細節之前,讓我們再次強調為什麼選擇 Terraform 進行 Grafana 資料來源的 IaC 管理:
我們的頂層結構設計基於兩個核心概念:設定模板(Configs)和資料源(Datasources)。

設定模板定義了可重複使用的資料來源設定。這種方法允許我們創建高度標準化的設定,可以跨多個資料來源實例共享。
設定範本(Configs)
configs 部分定義了可重複使用的資料來源設定範本:
configs = {
  prometheus_default = { ... }
  prometheus_custom = { ... }
  loki_default = { ... }
  tempo_default = { ... }
}
每個設定範本包含 json_data 和 secure_json_data,允許我們定義資料來源的具體設定和敏感資訊。
datasources 部分定義了實際的資料來源實例:
  datasources = {
    // Prometheus Datasource
    prometheus = {
      Prometheus-1 = {
        uid        = "prometheus"
        url        = "http://prometheus:9090"
        is_default = true
        config     = local.configs.prometheus_default
      }
      Prometheus-2 = {
        uid    = "prometheus-2"
        url    = "http://prometheus-2:9090"
        config = local.configs.prometheus_default
      }
    }
    // Loki Datasource
    loki = {
      Loki-default = {
        uid    = "loki"
        url    = "http://loki:3100"
        config = local.configs.loki_default
      }
    }
    // Tempo Datasource
    tempo = {
      Tempo-default = {
        uid    = "tempo"
        url    = "http://tempo:3200"
        config = local.configs.tempo_default
      }
    }
  }
這個結構的設計採用了多層嵌套的 map 結構,具有以下特點和優勢:
config = local.configs.prometheus_default 這樣的引用,可以輕鬆地在多個資料源之間共享設定。這減少了重複代碼,提高了維護性。這種結構允許我們為每種類型的資料源定義多個設定,每個資源都可以引用同一個設定範本。
現在,我們已經瞭解了 Grafana 中不同類型的資料源及其設定,接下來我們將結合先前所提到的全域動態生成資源的概念,實作我們的 Grafana 資料源 IaC 解決方案。這種方法將允許我們靈活地管理多種類型的資料源,並為每個資料源提供自定義的設定選項。
接下來我們實際執行指令建立 grafana_data_source 資源:
locals {
  datasources = {
    // Prometheus Datasource
    prometheus = {
      Prometheus-1 = {
        uid        = "prometheus"
        url        = "http://prometheus:9090"
        http_headers = {
          "X-Scope-OrgID" = "1"
        }
        is_default = true
        config     = local.configs.prometheus_default
      }
      Prometheus-2 = {
        uid    = "prometheus-2"
        url    = "http://prometheus-2:9090"
        http_headers = {
          "Authorization" = "Bearer your.prometheus_token"
        }
        config = local.configs.prometheus_default
      }
    }
    // Loki Datasource
    loki = {
      Loki-default = {
        uid    = "loki"
        url    = "http://loki:3100"
        http_headers = {
          "X-Custom-Header" = "LokiValue"
        }
        config = local.configs.loki_default
      }
    }
    // Tempo Datasource
    tempo = {
      Tempo-default = {
        uid    = "tempo"
        url    = "<http://tempo:3200>"
        config = local.configs.tempo_default
      }
    }
  }
  configs = {
    prometheus_default = {
      json_data = {
        http_method     = "POST"
        sigv4_auth      = false
        sigv4_auth_type = ""
        sigv4_region    = ""
      }
      secure_json_data = {}
    }
    prometheus_custom = {
      json_data = {
        http_method     = "GET"
        sigv4_auth      = true
        sigv4_auth_type = "default"
        sigv4_region    = "us-west-2"
      }
      secure_json_data = {
        access_key = "your-access-key"
        secret_key = "your-secret-key"
      }
    }
    loki_default = {
      json_data = {
        max_lines = 1000
        derived_fields = [
          {
            datasourceUid = "tempo"
            matcherRegex  = "[tT]race_?[iI][dD]\\"?[:=]\\"?(\\\\w+)"
            name          = "TraceID"
            url           = "$${__value.raw}"
          }
        ]
      }
      secure_json_data = {}
    }
    tempo_default = {
      json_data = {
        tracesToLogsV2 = {
          customQuery     = true
          datasourceUid   = "loki"
          filterBySpanID  = false
          filterByTraceID = false
          query           = "|=\\"$${__trace.traceId}\\" | json"
        }
      }
      secure_json_data = {}
    }
  }
}
resource "grafana_data_source" "datasources" {
  for_each = merge([
    for type, sources in local.datasources : {
      for name, source in sources : "${type}/${name}" => merge(source, { name = name, type = type })
    }
  ]...)
  type         = each.value.type
  name         = each.value.name
  uid          = each.value.uid
  url          = each.value.url
  is_default   = lookup(each.value, "is_default", false)
  http_headers = lookup(each.value, "http_headers", null)
  json_data_encoded = jsonencode(each.value.config.json_data)
  secure_json_data_encoded = jsonencode(each.value.config.secure_json_data)
}
這個 Terraform 資源展示了幾個進階技巧。它利用 for_each 和 merge 函數動態生成多個資料源,採用嵌套 map 結構組織配置,提高了代碼的可讀性和可維護性。通過 lookup 函數處理可選屬性,以及 jsonencode 函數處理複雜的 JSON 配置,使得資源定義更加靈活。這種方法大大簡化了 Grafana 資料源的管理,使得大規模部署和維護變得更加高效。
接著就讓我們實際執行指令建立資源:
terraform init
terraform apply
---
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
Terraform will perform the following actions:
  # grafana_data_source.datasources["loki/Loki-default"] will be created
  + resource "grafana_data_source" "datasources" {
      + access_mode              = "proxy"
      + basic_auth_enabled       = false
      + http_headers             = (sensitive value)
      + id                       = (known after apply)
      + is_default               = false
      + json_data_encoded        = jsonencode(
        // ...
        )
      + name                     = "Loki-default"
      + secure_json_data_encoded = (sensitive value)
      + type                     = "loki"
      + uid                      = "loki"
      + url                      = "<http://loki:3100>"
    }
  # grafana_data_source.datasources["prometheus/Prometheus-1"] will be created
  + resource "grafana_data_source" "datasources" {
      + access_mode              = "proxy"
      + basic_auth_enabled       = false
      + http_headers             = (sensitive value)
      + id                       = (known after apply)
      + is_default               = true
      + json_data_encoded        = jsonencode(
        // ...
        )
      + name                     = "Prometheus-1"
      + secure_json_data_encoded = (sensitive value)
      + type                     = "prometheus"
      + uid                      = "prometheus"
      + url                      = "<http://prometheus:9090>"
    }
  # grafana_data_source.datasources["prometheus/Prometheus-2"] will be created
  + resource "grafana_data_source" "datasources" {
      + access_mode              = "proxy"
      + basic_auth_enabled       = false
      + http_headers             = (sensitive value)
      + id                       = (known after apply)
      + is_default               = false
      + json_data_encoded        = jsonencode(
        // ...
        )
      + name                     = "Prometheus-2"
      + secure_json_data_encoded = (sensitive value)
      + type                     = "prometheus"
      + uid                      = "prometheus-2"
      + url                      = "<http://prometheus-2:9090>"
    }
  # grafana_data_source.datasources["tempo/Tempo-default"] will be created
  + resource "grafana_data_source" "datasources" {
      + access_mode              = "proxy"
      + basic_auth_enabled       = false
      + id                       = (known after apply)
      + is_default               = false
      + json_data_encoded        = jsonencode(
        // ...
        )
      + name                     = "Tempo-default"
      + secure_json_data_encoded = (sensitive value)
      + type                     = "tempo"
      + uid                      = "tempo"
      + url                      = "<http://tempo:3200>"
    }
Plan: 4 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.
  Enter a value: yes
grafana_data_source.datasources["tempo/Tempo-default"]: Creating...
grafana_data_source.datasources["loki/Loki-default"]: Creating...
grafana_data_source.datasources["prometheus/Prometheus-1"]: Creating...
grafana_data_source.datasources["prometheus/Prometheus-2"]: Creating...
grafana_data_source.datasources["tempo/Tempo-default"]: Creation complete after 0s [id=1:tempo]
grafana_data_source.datasources["loki/Loki-default"]: Creation complete after 0s [id=1:loki]
grafana_data_source.datasources["prometheus/Prometheus-1"]: Creation complete after 0s [id=1:prometheus]
grafana_data_source.datasources["prometheus/Prometheus-2"]: Creation complete after 0s [id=1:prometheus-2]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
接著我們到 Grafana 中確認是否成功建立:


在這個快速變化的技術世界中,我們的 Grafana 資料源管理方法不僅僅是一種技術實現,更是一種思維模式的轉變。通過將基礎設施即代碼(IaC)的理念與 Grafana 的強大功能相結合,我們開闢了一條通向更彈性的監控系統管理之路,為CI/CD 流程注入了更多可能性。
更重要的是,隨著資料量的爆炸性增長和監控需求的日益複雜化,這種方法提供了一個可擴展的框架,能夠輕鬆適應新的資料源類型和不斷演變的監控策略。它不僅提高了我們對系統的掌控能力,還為數據驅動決策提供了堅實的基礎,使得從海量數據中提取有價值的洞察變得更加容易。